iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0

前面我講10篇了,告訴你們coroutine是什麼,怎麼用,如何切thread,和她背後發生什麼事

其實有人要我寫那些內建的suspend function,但我覺得沒必要,你要是把基本概念搞懂了,那些內建的掛起函數,大多看一下描述就會用了,甚至不用看,比如說delay(),但如果你基礎沒搞懂,你用了也是懵懵懂懂的用,到了debug的時候,終究還是得把基礎搞懂

這篇,我會就前面10篇的所有內容,解答一些疑問,也會給出幾個例子講解,可以搭配之前的文章閱讀

thread的掛起和coroutine的掛起

首先要了解coroutine和thread是不同的東西,os不知道coroutine,而coroutine會把任務透過handler.post加入thread裡面我在這裡講過

那如果今天的任務是suspend的呢

suspend fun doTask(){
    withContext(Dispatcher.IO){
        val apiResult = apiCall()
        writeToRoom(apiResult)
    }
}
suspend fun apiCall(){

}
suspend fun writeToRoom(){

}

會變成

mHandler.post{
    doTask()
}

當Main thread執行到doTask時,會將任務切換到 IO thread執行,並在Main Thread終止doTask的coroutine,這樣Main thread就能繼續繪製ui,畫面也就不卡了

但在coruotine裡面的suspend就效果就不太一樣,apiCall和writeToRoom都是掛起函數,他們透過finite machine會被編譯成state machine,遞迴執行,並透過class共享參數,這邊看suspend編譯
而站在Thread調用者的角度,就是函數可被掛起,但狀態保留,並在耗時任務結束後返回值

併發和掛起,衝突嗎?還是完美情侶?

網上有種說法,coroutine比較高效,因為他能把api呼叫掛起,不用傻等,這樣thread就能去執行下一個api call,對嗎?

錯!!!
對thread來說,掛起是將任務切換到別的thread執行,對coroutine來說,掛起是編譯後照順序執行,任務每個都要做,而且得照順序做,並不會比較高效
而實際使用上,在main thread呼叫suspend function是很常見的,而我們通常用withContext確保main thread safe

lifecycleScope.launch {
    callDelay()
    callDelay()
    callDelay()
}
suspend fun callDelay(){
    withContext(Dispatchers.IO){
        delay(5000L)
        Timber.d(Thread.currentThread().name)
    }
}

2021-09-26 14:15:32.511 9530-9587/: DefaultDispatcher-worker-1
2021-09-26 14:15:37.516 9530-9587/: DefaultDispatcher-worker-1
2021-09-26 14:15:42.521 9530-9587/: DefaultDispatcher-worker-1

那併發呢?用併發加掛起不就可以不用傻等?thread的利用不就高效了?

還是錯!!併發執行任務可以減少等待的時間,但非常重要的一點,async會做什麼?
他會創造一個新的子coroutine,對吧?
那實際上在幹嘛,是由多個子couroutine在多個io thread同時做多個任務,不是他們在同一個io thread工作

寶傑,那這個例子,你怎麼看?

lifecycleScope.launch {
    val a = async (Dispatchers.Main){
        delay(2000L)
        Timber.d("a")
    }
    val b = async (Dispatchers.Main){
        delay(2000L)
        Timber.d("b")
    }
    val c = async (Dispatchers.Main) {
        delay(2000L)
        Timber.d("c")
    }
    awaitAll(a,b,c)
}
//2021-09-26 14:22:12.109 $a: a
//2021-09-26 14:22:12.111 $b: b
//2021-09-26 14:22:12.114 $c: c

我不是利用suspend高效的使用main thread了嗎?

並不是,delay()的底層是用java.Executor完成的
An {@code Executor} is normally used instead of explicitly creating threads.

source: kotlin.coroutines.Executors.kt
這邊就能連到java.Exectutor了

白話來說是什麼意思呢?
阻塞的東西被放到Exectutor執行了,結束後會再切回Main thread; 另一方面,這個代碼並不能表達所謂的高效,我們所謂的阻塞代碼應該是Thread.sleep(),而這樣就只會照順序進行了

cpu核心數量有限,為什麼可以併發那麼多

首先要了解cpu的運作速度,在pixel2上,單個cpu週期低於0.0000000004秒,一般人眨言大約花0.4秒,也就是說,你眨眼的時間cpu能運行10億個週期,還不算現在已經要出pixel6(好想要pixel6呀)
資料來源

這跟併發有關係嗎?大有關係

当线程处于IO操作时,线程是阻塞的,线程由运行状态切换到等待状态。此时CPU会做上下文切换,以便处理其他程序;当IO操作完成后,CPU会收到一个来自硬盘的中断信号,CPU正在执行的线程因此会被打断,回到ready队列。而先前因I/O而waiting的线程随着I/O的完成也再次回到就绪队列,此时CPU可能会选择他执行。
轉自騰訊雲

thread能再利用嗎?

看情況,thread在任務執行完後,有其他任務就會繼續執行其他任務,沒有的話,就會被回收

scope取消了,那工作還做嗎?

fun testScope(){
    scope.launch {
        while (true){
            //work
        }
    }
}

那呼叫cancel後,工作還會做嗎?
會,記得嗎?coroutine會等待的工作結束後才返回,那要怎麼辦呢?
讓他每次執行前都檢查isActive,這邊能直接改成while(isActive)
executor

稍微聊聊dispatcher

Dispatcher.Main - 因為篇幅跟新手取向的關係,這部分我也就不講了,只講結果,耗時工作如果跑在main thread,畫面會freeze,並發生ANR(application not response), 因為main thread會負責繪製ui

Dispatcher.IO - I/O thread顧名思義,我們大多用來讀寫操作,從資料庫抓資料、api資料等等,可以很多很多個

Dispatcher.Default - 之前提過Default的use case是 大量運用cpu資源運算的工作,如資料排序或解析json,DiffUtil等等,但Default和IO有著不小的關係,必須了解一下,所以又翻了文檔

The default CoroutineDispatcher that is used by all standard builders like launch, async, etc if neither a dispatcher nor any other ContinuationInterceptor is specified in their context.

It is backed by a shared pool of threads on JVM. By default, the maximum number of threads used by this dispatcher is equal to the number of CPU cores, but is at least two.

雖然英文不長,簡單說就是launch/ async建構子預設是走default, 數量最多和CPU核心數量一樣多,最少兩個,那跟IO也沒關係呀,有的,要去看source code,這邊我就不帶了,請看jast的文章,這邊節錄重點 IO 是基於 Default 去建立的,IO 與 Default 的差別在於 Default 開的 thread 會受到 CPU core 限制,而 IO 則會比 Default 多上不少。

Dispatcher.Unconfined - 除了yield以外,原本在哪個thread就跑在哪個thread,這個特性很重要 文檔,也有例外(yield)在jast那篇

我是誰我在哪? suspend和withContext切換disatcher

第一關

lifecycleScope.launch {
    Timber.d( "${Thread.currentThread().name}" )
    withContext(Dispatchers.Default){
        Timber.d( "${Thread.currentThread().name}" )
    }
    withContext(Dispatchers.Main){
        Timber.d( "${Thread.currentThread().name}" )
    }
    withContext(Dispatchers.IO){
        Timber.d( "${Thread.currentThread().name}" )
    }
    withContext(Dispatchers.Unconfined){
        Timber.d( "${Thread.currentThread().name}" )
    }
}

/**
 * main
 * DefaultDispatcher-worker-1
 * main
 * DefaultDispatcher-worker-1
 * main
 * */

記得吧,unConfined通常會跑在正在跑的Thread,以及io和default的關係

第二關

lifecycleScope.launch {
    Timber.d( "${Thread.currentThread().name}" )

    withContext(Dispatchers.Default){
        launch (Dispatchers.Unconfined){
            Timber.d( "${Thread.currentThread().name}" )
            withContext(Dispatchers.Main){
                Timber.d( "${Thread.currentThread().name}" )
            }
            Timber.d( "${Thread.currentThread().name}" )
        }
    }

}

/**
 * main //lifecycle
 * DefaultDispatcher-worker-1 //Unconfined
 * main //Main
 * main //Unconfined
 * */

解釋一下,第二個Unconfined會在main thread的原因之前講過了,withContext結束後,會invoke Continuation(Unconfined),但因為他本身特性,所以會直接跑在Main thread

但如果今天我們把withContext換成launch,launch是創造了child coroutine,而不是切換thread,所以會變成

withContext(Dispatchers.Default){
    launch (Dispatchers.Unconfined){
        Timber.d( "${Thread.currentThread().name}" )
        launch (Dispatchers.Main){
            Timber.d( "${Thread.currentThread().name}" )
        }
        Timber.d( "${Thread.currentThread().name}" )
    }
}
/**
 * main //lifecycle
 * DefaultDispatcher-worker-1 //Unconfined
 * main //Main
 * DefaultDispatcher-worker-1 //Unconfined
 * */

到這邊,大家應該都懂了thread會怎麼切換了吧

連結統整

必看

jast的文章
Coroutine context and dispatchers

選看

轉自騰訊雲
executor


上一篇
聊聊structure concurrency 結構化併發
下一篇
day12 輕鬆一下,用 coroutine 接個 restful api
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言